/***
 * Copyright 2002-2010 jamod development team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***/

package net.wimpi.modbus.io;

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.ModbusIOException;
import net.wimpi.modbus.msg.ModbusMessage;
import net.wimpi.modbus.msg.ModbusRequest;
import net.wimpi.modbus.msg.ModbusResponse;
import net.wimpi.modbus.util.ModbusUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Class that implements the ModbusRTU transport
 * flavor.
 *
 * @author John Charlton
 * @author Dieter Wimberger
 *
 * @version @version@ (@date@)
 */
public class ModbusRTUTransport extends ModbusSerialTransport {

    private InputStream m_InputStream;          //wrap into filter input
    private OutputStream m_OutputStream;        //wrap into filter output

    private byte[] m_InBuffer;
    private BytesInputStream m_ByteIn;         //to read message from
    private BytesOutputStream m_ByteInOut;     //to buffer message to
    private BytesOutputStream m_ByteOut;      //write frames
    private byte[] lastRequest = null;

    public void writeMessage(ModbusMessage msg) throws ModbusIOException {
        try {
            int len;
            synchronized (m_ByteOut) {
                // first clear any input from the receive buffer to prepare
                // for the reply since RTU doesn't have message delimiters
                clearInput();
                //write message to byte out
                m_ByteOut.reset();
                msg.setHeadless();
                msg.writeTo(m_ByteOut);
                len = m_ByteOut.size();
                int[] crc = ModbusUtil.calculateCRC(m_ByteOut.getBuffer(), 0, len);
                m_ByteOut.writeByte(crc[0]);
                m_ByteOut.writeByte(crc[1]);
                //write message
                len = m_ByteOut.size();
                byte buf[] = m_ByteOut.getBuffer();
                m_OutputStream.write(buf, 0, len);     //PDU + CRC
                m_OutputStream.flush();
                if(Modbus.debug) System.out.println("Sent: " + ModbusUtil.toHex(buf, 0, len));
                // clears out the echoed message
                // for RS485
                if (m_Echo) {
                    readEcho(len);
                }
                lastRequest = new byte[len];
                System.arraycopy(buf, 0, lastRequest, 0, len);
            }

        } catch (Exception ex) {
            if (Modbus.debug) {
                System.err.println(ex.getMessage());
            }
            throw new ModbusIOException("I/O failed to write: " + ex.getLocalizedMessage());
        }

    }//writeMessage

    //This is required for the slave that is not supported
    public ModbusRequest readRequest() throws ModbusIOException {
        throw new RuntimeException("Operation not supported.");
    } //readRequest

    /**
     * Clear the input if characters are found in the input stream.
     *
     * @throws java.io.IOException
     */
    public void clearInput() throws IOException {
        if (m_InputStream.available() > 0) {
            int len = m_InputStream.available();
            byte buf[] = new byte[len];
            m_InputStream.read(buf, 0, len);
            if(Modbus.debug) System.out.println("Clear input: " +
                    ModbusUtil.toHex(buf, 0, len));
        }
    }//cleanInput

    public ModbusResponse readResponse() throws ModbusIOException {

        boolean done = false;
        ModbusResponse response = null;
        int dlength = 0;

        try {
            do {
                //1. read to function code, create request and read function specific bytes
                synchronized (m_ByteIn) {
                    int uid = m_InputStream.read();
                    if (uid != -1) {
                        int fc = m_InputStream.read();
                        m_ByteInOut.reset();
                        m_ByteInOut.writeByte(uid);
                        m_ByteInOut.writeByte(fc);

                        //create response to acquire length of message
                        response = ModbusResponse.createModbusResponse(fc);
                        response.setHeadless();

                        // With Modbus RTU, there is no end frame.  Either we assume
                        // the message is complete as is or we must do function
                        // specific processing to know the correct length.  To avoid
                        // moving frame timing to the serial input functions, we set the
                        // timeout and to message specific parsing to read a response.
                        getResponse(fc, m_ByteInOut);
                        dlength = m_ByteInOut.size() - 2; // less the crc
                        if (Modbus.debug) System.out.println("Response: " + ModbusUtil.toHex(m_ByteInOut.getBuffer(), 0, dlength + 2));

                        m_ByteIn.reset(m_InBuffer, dlength);

                        //check CRC
                        int[] crc = ModbusUtil.calculateCRC(m_InBuffer, 0, dlength); //does not include CRC
                        if (ModbusUtil.unsignedByteToInt(m_InBuffer[dlength]) != crc[0] || ModbusUtil.unsignedByteToInt(m_InBuffer[dlength + 1]) != crc[1]) {
                            throw new IOException("CRC Error in received frame: " + dlength + " bytes: " + ModbusUtil.toHex(m_ByteIn.getBuffer(), 0, dlength));
                        }
                    } else {
                        throw new IOException("Error reading response.");
                    }

                    //read response
                    m_ByteIn.reset(m_InBuffer, dlength);
                    if (response != null) {
                        response.readFrom(m_ByteIn);
                    }
                    done = true;
                }//synchronized
            } while (!done);
            return response;
        } catch (Exception ex) {
            if (Modbus.debug) {
                System.err.println("Last request: " + ModbusUtil.toHex(lastRequest));
                System.err.println(ex.getMessage());
                ex.printStackTrace();
            }
            throw new ModbusIOException("I/O exception - failed to read: " + ex.getLocalizedMessage());
        }
    }//readResponse

    /**
     * Prepares the input and output streams of this
     * <tt>ModbusRTUTransport</tt> instance.
     *
     * @param in the input stream to be read from.
     * @param out the output stream to write to.
     * @throws java.io.IOException if an I\O error occurs.
     */
    public void prepareStreams(InputStream in, OutputStream out)
            throws IOException {
        m_InputStream = in;   //new RTUInputStream(in);
        m_OutputStream = out;

        m_ByteOut = new BytesOutputStream(Modbus.MAX_MESSAGE_LENGTH);
        m_InBuffer = new byte[Modbus.MAX_MESSAGE_LENGTH];
        m_ByteIn = new BytesInputStream(m_InBuffer);
        m_ByteInOut = new BytesOutputStream(m_InBuffer);
    } //prepareStreams

    public void close() throws IOException {
        m_InputStream.close();
        m_OutputStream.close();
    }//close

    private void getResponse(int fn, BytesOutputStream out) throws IOException {
        int bc = -1, bc2 = -1, bcw = -1;
        int inpBytes = 0;
        byte inpBuf[] = new byte[256];

        try {
            switch (fn) {
                case 0x01:
                case 0x02:
                case 0x03:
                case 0x04:
                case 0x0C:
                case 0x11:  // report slave ID version and run/stop state
                case 0x14:  // read log entry (60000 memory reference)
                case 0x15:  // write log entry (60000 memory reference)
                case 0x17:
                    // read the byte count;
                    bc = m_InputStream.read();
                    out.write(bc);
                    // now get the specified number of bytes and the 2 CRC bytes
                    setReceiveThreshold(bc+2);
                    inpBytes = m_InputStream.read(inpBuf, 0, bc+2);
                    out.write(inpBuf, 0, inpBytes);
                    m_CommPort.disableReceiveThreshold();
                    if (inpBytes != bc+2 && Modbus.debug) {
                        System.out.println("Error: looking for " + (bc+2) + " bytes, received " + inpBytes);
                    }
                    break;
                case 0x05:
                case 0x06:
                case 0x0B:
                case 0x0F:
                case 0x10:
                    // read status: only the CRC remains after address and function code
                    setReceiveThreshold(6);
                    inpBytes = m_InputStream.read(inpBuf, 0, 6);
                    out.write(inpBuf, 0, inpBytes);
                    m_CommPort.disableReceiveThreshold();
                    break;
                case 0x07:
                case 0x08:
                    // read status: only the CRC remains after address and function code
                    setReceiveThreshold(3);
                    inpBytes = m_InputStream.read(inpBuf, 0, 3);
                    out.write(inpBuf, 0, inpBytes);
                    m_CommPort.disableReceiveThreshold();
                    break;
                case 0x16:
                    // eight bytes in addition to the address and function codes
                    setReceiveThreshold(8);
                    inpBytes = m_InputStream.read(inpBuf, 0, 8);
                    out.write(inpBuf, 0, inpBytes);
                    m_CommPort.disableReceiveThreshold();
                    break;
                case 0x18:
                    // read the byte count word
                    bc = m_InputStream.read();
                    out.write(bc);
                    bc2 = m_InputStream.read();
                    out.write(bc2);
                    bcw = ModbusUtil.makeWord(bc, bc2);
                    // now get the specified number of bytes and the 2 CRC bytes
                    setReceiveThreshold(bcw+2);
                    inpBytes = m_InputStream.read(inpBuf, 0, bcw + 2);
                    out.write(inpBuf, 0, inpBytes);
                    m_CommPort.disableReceiveThreshold();
                    break;
            }
        } catch (IOException e) {
            m_CommPort.disableReceiveThreshold();
            throw new IOException("getResponse serial port exception");
        }
    }//getResponse

} //ModbusRTUTransport
